package com.inexas.cu.stratus;

import java.lang.reflect.*;
import java.util.*;
import javax.jdo.*;
import org.antlr.runtime.*;
import com.inexas.util.*;

public class Constraints<T> {
	public static class Filter<T> {
		private static class Evaluator {
			private enum Operator { GT, GTE, EQ, LTE, LT }
			final String identifier, literal;
			final Operator operator;

			Evaluator(String identifier, String operator, String literal) {
				this.identifier = identifier;
				final char firstChar = operator.charAt(0);
				final int length = operator.length();
				if(firstChar == '<') {
					this.operator = length == 1 ? Operator.LT : Operator.LTE;
				} else if(firstChar == '>') {
					this.operator = length == 1 ? Operator.GT : Operator.GTE;
				} else {
					this.operator = Operator.EQ;
				}
				final char firstCharacter = literal.charAt(0);
				final boolean isString = firstCharacter == '\'' || firstCharacter == '"';
				this.literal = isString ? StringUtilities.stripQuotes(literal) : literal;
			}

			@Override
            public String toString() {
				return "Evaluator(" + identifier + ", " + operator + ", " + literal + ')';
            }
		}

		private final List<Evaluator> evaluators = new ArrayList<Evaluator>();

        public boolean accepts(T t) {
			boolean result = true;
			for(final Evaluator evaluator : evaluators) {
				try {
					@SuppressWarnings("unchecked")
					final Class<T> entityType = (Class<T>)t.getClass();
					final Field field = entityType.getDeclaredField(evaluator.identifier);
					field.setAccessible(true);
					@SuppressWarnings("unchecked")
					final Comparable<Object> comparator = (Comparable<Object>)field.get(t);
					
					final Object value;
					final Class<?> type = field.getType();
					// The if's are roughly in the order of popularity
					if(type == Integer.class || type == Integer.TYPE) {
						value = new Integer(evaluator.literal);
					} else if(type == String.class ) {
						value = evaluator.literal;
					} else if(type == Boolean.class || type == Boolean.TYPE) {
						value = new Boolean(evaluator.literal);
					} else if(type == Date.class) {
						value = DateUtilities.toDate(evaluator.literal);
					} else if(type == Character.class || type == Character.TYPE) {
						final String literal = evaluator.literal;
						final char[] ca = literal.toCharArray();
						final char c;
						if(ca.length == 3 && (ca[0] == '"' || ca[0] == '\'')) {
							c = ca[1];
						} else {
							c = (char)Integer.parseInt(literal);
						}
						value = new Character(c);
					} else if(type == Double.class || type == Double.TYPE) {
						value = new Double(evaluator.literal);
					} else if(type == Long.class || type == Long.TYPE) {
						value = new Long(evaluator.literal);
					} else if(type == Float.class || type == Float.TYPE) {
						value = new Float(evaluator.literal);
					} else if(type == Byte.class || type == Byte.TYPE) {
						value = new Byte(evaluator.literal);
					} else if(type == Short.class || type == Short.TYPE) {
						value = new Short(evaluator.literal);
					} else {
						final String message = "Type not handled: " + type.getName();
						throw new StratusException(message);
					}
					final int comparisson = comparator.compareTo(value);
					switch(evaluator.operator) {
					case GT:
						result = comparisson > 0;
						break;
					case GTE:
						result = comparisson >= 0;
						break;
					case EQ:
						result = comparisson == 0;
						break;
					case LTE:
						result = comparisson <= 0;
						break;
					case LT:
						result = comparisson < 0;
						break;
					}
					if(!result) {
						break;
					}
				} catch(final Exception e) {
					throw new StratusException( //
					        "Error filtering object: " + //
			                evaluator.toString() + ' ' +	//
			                t.toString(), //
					        e);
				}
			}
			return result;
		}

		public void add(String identifier, String operator, String literal) {
			evaluators.add(new Evaluator(identifier, operator, literal));
		}
	}

	private static class MyComparator<T> implements Comparator<T> {
		private static class Sorter {
			final String identifier;
			final boolean ascending;

			Sorter(String identifier, boolean acending) {
				this.identifier = identifier;
				this.ascending = acending;
			}

			@Override
            public String toString() {
				return "Sorter(" + identifier + ", " + ascending + ')';
            }
		}

		private final List<Sorter> sorters = new ArrayList<Sorter>();

		void add(String identifier, boolean ascending) {
			sorters.add(new Sorter(identifier, ascending));
		}

		public int compare(T lhs, T rhs) {
			assert lhs != null && rhs != null;
			assert lhs.getClass() == rhs.getClass();

			int result = 0; // Assume lhs == rhs
			for(final Sorter sorter : sorters) {
				try {
					final Field lhsField = lhs.getClass().getDeclaredField(sorter.identifier);
					lhsField.setAccessible(true);
					final Field rhsField = rhs.getClass().getDeclaredField(sorter.identifier);
					rhsField.setAccessible(true);
					@SuppressWarnings("unchecked")
					final Comparable<Object> lhsComparator = (Comparable<Object>)lhsField.get(lhs);
					result = lhsComparator.compareTo(rhsField.get(rhs));
					if(result != 0) {
						if(!sorter.ascending) {
							result = -result;
						}
						break;
					}
				} catch(final Exception e) {
					throw new StratusException( //
					        "Error comparing objects: " +	//
					        sorters.toString() + ' '	//
					        + lhs.toString() + '/' + rhs.toString(), //
					        e);
				}
			}
			return result;
		}

	}

	private final String filterString;
	private final String orderingString;
	private final Filter<T> filter = new Filter<T>();
	private final MyComparator<T> comparator = new MyComparator<T>();

	public Constraints(String... constraints) {
		final StringBuilder filterSb = new StringBuilder();
		String filterDelimiter = "";
		final StringBuilder orderingSb = new StringBuilder();
		String orderingDelimiter = "";

		for(final String constraint : constraints) {

			final ANTLRStringStream input = new ANTLRStringStream(constraint);
			final StratusLexer lexer = new StratusLexer(input);
			final CommonTokenStream tokens = new CommonTokenStream(lexer);
			final StratusParser parser = new StratusParser(tokens);
			try {
	            parser.parse();
	            if(parser.getNumberOfSyntaxErrors() > 0) {
	            	throw new StratusException("Syntax error parsing: " + constraint);
	            }
            } catch(final RecognitionException e) {
            	throw new StratusException("Error parsing: " + constraint, e);
            }
			final String identifier = parser.identifier;
			final String literal = parser.literal;
			if(literal == null) { // It's an ordering

				// Update the query string...
				orderingSb.append(orderingDelimiter);
				orderingDelimiter = ",";
				orderingSb.append(identifier);
				final char direction = parser.direction;
				final boolean ascending = direction != '-';
				if(!ascending) {
					orderingSb.append(" DESC");
				}

				// Update the comparator...
				comparator.add(identifier, ascending);
			} else { // It's a filter

				// Update the filter string...
				filterSb.append(filterDelimiter);
				filterDelimiter = "&&";
				filterSb.append(identifier);
				final String operator = parser.operator;
				filterSb.append(operator.charAt(0) == ':' ? "==" : operator);
				// !todo Look after different data types here
				filterSb.append(literal);

				// Update the filter...
				filter.add(identifier, operator, literal);
			}
		}
		filterString = filterSb.length() == 0 ? null : filterSb.toString();
		orderingString = orderingSb.length() == 0 ? null : orderingSb.toString();
	}

	public Query getQuery(PersistenceManager pm, Class<T> entityType) {
		final Query result = pm.newQuery(entityType);
		if(filterString != null) {
			result.setFilter(filterString);
		}
		if(orderingString != null) {
			result.setOrdering(orderingString);
		}
		return result;
	}

	public List<T> constrain(Iterator<T> all) {
		final List<T> result = new ArrayList<T>();
		if(filter != null) {
			while(all.hasNext()) {
				final T t = all.next();
				if(filter.accepts(t)) {
					result.add(t);
				}
			}
		} else {
			while(all.hasNext()) {
				final T t = all.next();
				result.add(t);
			}
		}
		
		if(comparator != null) {
			Collections.sort(result, comparator);
		}
		return result;
    }

	public List<T> constrain(List<T> list) {
		final List<T> result = new ArrayList<T>();
		if(filter != null) {
			for(final T t : list) {
				if(filter.accepts(t)) {
					result.add(t);
				}
			}
		} else {
			result.addAll(list);
		}
		
		if(comparator != null) {
			Collections.sort(result, comparator);
		}
		return result;
    }

	public boolean accepts(T t) {
		final boolean result;
		if(filter == null) {
			result = true;
		} else {
			result = filter.accepts(t);
		}
		return result;
    }

}
